package com.baizhi.shiro;

import com.baizhi.annotation.GetCacheAnnotation;
import com.baizhi.dao.CmfzAdminDao;
import com.baizhi.entity.CmfzAdmin;
import com.baizhi.service.CmfzShiroService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;

/*
* 这个类是自定义的 授权realm类
* 和前面写的自定义认证realm类ShiroRealm类似  都是为了修改shiro框架默认的认证授权“数据源”
* shiro框架默认的认证授权“数据源”是从shiro的配置文件中获取   我们想要的是从真实数据库表中获取
*
* 和自定义的 认证realm类不同   授权realm类需要继承的是授权的抽象类AuthorizingRealm
*                               并且要重写其中的两个方法
*  方法一：获取授权信息的方法（将从数据库查询的授权信息进行封装返回）
*  方法二：获取认证信息的方法（这个方法与我们前面写的自定义认证realm类中重写的方法一模一样，所以
*           我们今天写了这个 授权realm类后  就不需要前面写的认证realm类了   并且要在ShiroConfig配置类
*           中，将其中对自定义的认证realm类的配置  都改成这个自定义的 授权realm类）
* */
@Slf4j
public class AuthorRealm extends AuthorizingRealm {
    //首先，要装配一个管理员dao对象  用于查询数据库
    @Autowired
    private CmfzAdminDao cmfzAdminDao;
    @Autowired
    private CmfzShiroService cmfzShiroService;//根据账户名查询其拥有的角色信息或权限信息

    /*
    * 重写获取授权信息的方法  参数：认证成功后封装了认证信息的数据  可以通过这个参数获取这个要授权的账户名
    * */
    @Override
    @GetCacheAnnotation//将该方法查询的数据缓存到redis中
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //1、从参数中获取认证成功的这个主体的主身份getPrimaryPrincipal()  本项目就是账户名
        String username = principal.getPrimaryPrincipal().toString();
        //2、根据管理员的账户名  调用业务方法  查询该该账户对应的角色和权限信息
        Set<String> roles = cmfzShiroService.selectRoleNameByUsername(username);//角色信息
        log.info("自定义realm类中，获取授权信息方法中查询到的角色信息："+roles);
        Set<String> permissions = cmfzShiroService.selectResourcePermission(username);//权限字符串信息
        log.info("自定义realm类中，获取授权信息方法中查询到的权限信息："+permissions);
        //3、封装上面获得的授权信息   这里不用非空判断  因为只要认证成功的账户一定会有角色 是角色就一定有权限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        /*
        * 注意：这个封装的授权信息对象中，要求封装的角色信息和权限信息都是Set<String>类型的
        *       所以要求业务层查询角色和权限的方法的返回值是Set<String>类型的
        * */
        info.setRoles(roles);
        info.setStringPermissions(permissions);
        //4、最后返回封装好的授权信息
        /*然后还需要去ShiroConfig类（shiro框架的配置类）中  对该自定义授权realm类进行配置
        *最后就可以在想要授权的模块的业务层方法中进行授权了【这属于代码式授权】
        *   或者在jsp页面中使用 “标签式授权”  对页面中的某些元素（如删除、添加按钮、某些表格等等）
        * */
        return info;
    }


    /*
    * 重写获取认证信息的方法   参数：封装页面传递的认证数据的令牌对象token
    * */
    @Override
    @GetCacheAnnotation//将该方法查询的数据缓存到redis中
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1、从参数token中获取要认证的管理员账号名   需要将参数token强转为UsernamePasswordToken
        UsernamePasswordToken token1 = (UsernamePasswordToken) token;
        String username = token1.getUsername();
        //2、根据用户名 从数据库中查询对应的管理员对象
        CmfzAdmin cmfzAdmin = cmfzAdminDao.selectOne(new QueryWrapper<CmfzAdmin>().eq("username", username));
        log.info("自定义授权realm类中，查询到的认证账户信息："+cmfzAdmin);
        //3、判断从数据库的查询结果  若是结果为null  则直接返回null  否则说明认证成功，要封装认证信息返回
        if(cmfzAdmin!=null){
            /*
            * 补：进入该if模块  说明认证成功  可以将这个管理员对象存到session中以备后用
            *       但一般为了安全，通常不会采用session   那么项目其它地方若想使用登陆标识，可以
            *       直接使用认证成功的管理员账户名   因为shiro框架可以在认证成功后，通过一个API在项目
            *       的任意处获取该认证信息中的账户名(subject.getPrincipal())
            *       今天这里采用session  因为这个项目的  日志aop类中  需要使用管理员的id  并且不用太考虑安全问题
            * */
            //首先，通过shiro的工具类SecurityUtils获取主体subject对象
            Subject subject = SecurityUtils.getSubject();
            // 然后获取session对象  保存登陆标识为管理员对象
            subject.getSession().setAttribute("login",cmfzAdmin);
           /* String principal = subject.getPrincipal().toString();  可以在项目的任意处获取认证成功后的账户名*/

            /*
            * 在这个if代码块中  最重要的其实是：对认证信息的封装并返回
            * 用的的方法是new SimpleAccount()  该方法就是新建一个简单账户  重点是其需要4个参数
            * 参数1：认证成功时从数据库查询的账户名
            * 参数2：从数据库查询的密码
            * 参数3：从数据库中查询的盐值（需要做ByteSource处理）  【若是没有经过加密，则无需此参数】
            * 参数4：当前对象的名字 this.getName()
            * */
            //首先  对查询到的盐值进行处理
            String salt = cmfzAdmin.getSalt();
            ByteSource bytes = ByteSource.Util.bytes(salt);
            return new SimpleAccount(cmfzAdmin.getUsername(),cmfzAdmin.getPassword(),bytes,this.getName());
        }
        return null;    //这个即是认证失败的返回值
    }
}
